home *** CD-ROM | disk | FTP | other *** search
- package File::Path;
-
- =head1 NAME
-
- File::Path - create or remove directory trees
-
- =head1 SYNOPSIS
-
- use File::Path;
-
- mkpath(['/foo/bar/baz', 'blurfl/quux'], 1, 0711);
- rmtree(['foo/bar/baz', 'blurfl/quux'], 1, 1);
-
- =head1 DESCRIPTION
-
- The C<mkpath> function provides a convenient way to create directories, even
- if your C<mkdir> kernel call won't create more than one level of directory at
- a time. C<mkpath> takes three arguments:
-
- =over 4
-
- =item *
-
- the name of the path to create, or a reference
- to a list of paths to create,
-
- =item *
-
- a boolean value, which if FALSE (the default for non-root users) will
- cause C<rmtree> to adjust the mode of directories (if required) prior
- to attempting to remove the contents. Note that on interruption or
- failure of C<rmtree>, directories may be left with more permissi
- modes for the owner.
-
- =item *
-
- the numeric mode to use when creating the directories
- (defaults to 0777), to be modified by the current umask.
-
- =back
-
- It returns a list of all directories (including intermediates, determined
- using the Unix '/' separator) created.
-
- If a system error prevents a directory from being created, then the
- C<mkpath> function throws a fatal error with C<Carp::croak>. This error
- can be trapped with an C<eval> block:
-
- eval { mkpath($dir) };
- if ($@) {
- print "Couldn't create $dir: $@";
- }
-
- Similarly, the C<rmtree> function provides a convenient way to delete a
- subtree from the directory structure, much like the Unix command C<rm -r>.
- C<rmtree> takes three arguments:
-
- =over 4
-
- =item *
-
- the root of the subtree to delete, or a reference to
- a list of roots. All of the files and directories
- below each root, as well as the roots themselves,
- will be deleted.
-
- =item *
-
- a boolean value, which if TRUE will cause C<rmtree> to
- print a message each time it examines a file, giving the
- name of the file, and indicating whether it's using C<rmdir>
- or C<unlink> to remove it, or that it's skipping it.
- (defaults to FALSE)
-
- =item *
-
- a boolean value, which if TRUE will cause C<rmtree> to
- skip any files to which you do not have delete access
- (if running under VMS) or write access (if running
- under another OS). This will change in the future when
- a criterion for 'delete permission' under OSs other
- than VMS is settled. (defaults to FALSE)
-
- =back
-
- It returns the number of files successfully deleted. Symlinks are
- simply deleted and not followed.
-
- B<NOTE:> There are race conditions internal to the implementation of
- C<rmtree> making it unsafe to use on directory trees which may be
- altered or moved while C<rmtree> is running, and in particular on any
- directory trees with any path components or subdirectories potentially
- writable by untrusted users.
-
- Additionally, if the third parameter is not TRUE and C<rmtree> is
- interrupted, it may leave files and directories with permissions altered
- to allow deletion (and older versions of this module would even set
- files and directories to world-read/writable!)
-
- Note also that the occurrence of errors in C<rmtree> can be determined I<only>
- by trapping diagnostic messages using C<$SIG{__WARN__}>; it is not apparent
- from the return value.
-
- =head1 DIAGNOSTICS
-
- =over 4
-
- =item *
-
- On Windows, if C<mkpath> gives you the warning: B<No such file or
- directory>, this may mean that you've exceeded your filesystem's
- maximum path length.
-
- =back
-
- =head1 AUTHORS
-
- Tim Bunce <F<Tim.Bunce@ig.co.uk>> and
- Charles Bailey <F<bailey@newman.upenn.edu>>
-
- =cut
-
- use 5.006;
- use Carp;
- use File::Basename ();
- use Exporter ();
- use strict;
- use warnings;
- use Cwd 'getcwd';
-
- our $VERSION = "1.07";
- our @ISA = qw( Exporter );
- our @EXPORT = qw( mkpath rmtree );
-
- my $Is_VMS = $^O eq 'VMS';
- my $Is_MacOS = $^O eq 'MacOS';
-
- # These OSes complain if you want to remove a file that you have no
- # write permission to:
- my $force_writeable = ($^O eq 'os2' || $^O eq 'dos' || $^O eq 'MSWin32' ||
- $^O eq 'amigaos' || $^O eq 'MacOS' || $^O eq 'epoc');
-
- sub mkpath {
- my($paths, $verbose, $mode) = @_;
- # $paths -- either a path string or ref to list of paths
- # $verbose -- optional print "mkdir $path" for each directory created
- # $mode -- optional permissions, defaults to 0777
- local($")=$Is_MacOS ? ":" : "/";
- $mode = 0777 unless defined($mode);
- $paths = [$paths] unless ref $paths;
- my(@created,$path);
- foreach $path (@$paths) {
- $path .= '/' if $^O eq 'os2' and $path =~ /^\w:\z/s; # feature of CRT
- # Logic wants Unix paths, so go with the flow.
- if ($Is_VMS) {
- next if $path eq '/';
- $path = VMS::Filespec::unixify($path);
- if ($path =~ m:^(/[^/]+)/?\z:) {
- $path = $1.'/000000';
- }
- }
- next if -d $path;
- my $parent = File::Basename::dirname($path);
- unless (-d $parent or $path eq $parent) {
- push(@created,mkpath($parent, $verbose, $mode));
- }
- print "mkdir $path\n" if $verbose;
- unless (mkdir($path,$mode)) {
- my $e = $!;
- # allow for another process to have created it meanwhile
- croak "mkdir $path: $e" unless -d $path;
- }
- push(@created, $path);
- }
- @created;
- }
-
- sub _rmtree;
- sub _rmtree
- {
-
- my ($path, $prefix, $up, $up_dev, $up_ino, $verbose, $safe) = @_;
-
- my ($dev, $ino) = lstat $path or do {
- carp "Can't stat $prefix$path ($!)" unless $!{ENOENT};
- return 0;
- };
-
- unless (-d _)
- {
- print "unlink $prefix$path\n" if $verbose;
- unless (unlink $path)
- {
- carp "Can't remove file $prefix$path ($!)";
- return 0;
- }
- return 1;
- }
-
- unless (chdir $path)
- {
- carp "Can't chdir to $prefix$path ($!)";
- return 0;
- }
-
- # avoid a race condition where a directory may be replaced by a
- # symlink between the lstat and the chdir
- my ($new_dev, $new_ino, $perm) = stat '.';
- unless ("$new_dev:$new_ino" eq "$dev:$ino")
- {
- croak "Directory $prefix$path changed before chdir, aborting";
- }
-
- $perm &= 07777;
- my $nperm = $perm | 0700;
- unless ($safe or $nperm == $perm or chmod $nperm, '.')
- {
- carp "Can't make directory $prefix$path read+writeable ($!)";
- $nperm = $perm;
- }
-
- my $count = 0;
- if (opendir my $dir, '.')
- {
- my $entry;
- while (defined ($entry = readdir $dir))
- {
- next if $entry =~ /^\.\.?$/;
- $entry =~ /^(.*)$/s; $entry = $1; # untaint
- $count += _rmtree $entry, "$prefix$path/", '..', $dev, $ino,
- $verbose, $safe;
- }
-
- closedir $dir;
- }
-
- # restore directory permissions if required (in case the rmdir
- # below fails) now, while we're still in the directory and may do
- # so without a race via '.'
- unless ($nperm == $perm or chmod $perm, '.')
- {
- carp "Can't restore permissions on directory $prefix$path ($!)";
- }
-
- # don't leave the caller in an unexpected directory
- unless (chdir $up)
- {
- croak "Can't return to $up from $prefix$path ($!)";
- }
-
- # ensure that a chdir .. didn't take us somewhere other than
- # where we expected (see CVE-2002-0435)
- unless (($new_dev, $new_ino) = stat '.'
- and "$new_dev:$new_ino" eq "$up_dev:$up_ino")
- {
- croak "Previous directory $up changed since entering $prefix$path";
- }
-
- print "rmdir $prefix$path\n" if $verbose;
- if (rmdir $path)
- {
- $count++;
- }
- else
- {
- carp "Can't remove directory $prefix$path ($!)";
- }
-
- return $count;
- }
-
- sub rmtree
- {
- my ($p, $verbose, $safe) = @_;
- $p = [] unless defined $p and length $p;
- $p = [ $p ] unless ref $p;
- my @paths = grep defined && length, @$p;
-
- # default to "unsafe" for non-root (will chmod dirs)
- $safe = $> ? 0 : 1 unless defined $safe;
-
- unless (@paths)
- {
- carp "No root path(s) specified";
- return;
- }
-
- my $oldpwd = getcwd or do {
- carp "Can't fetch initial working directory";
- return;
- };
-
- my ($dev, $ino) = stat '.' or do {
- carp "Can't stat initial working directory";
- return;
- };
-
- # untaint
- for ($oldpwd) { /^(.*)$/s; $_ = $1 }
-
- my $count = 0;
- for my $path (@paths)
- {
- $count += _rmtree $path, '', $oldpwd, $dev, $ino, $verbose, $safe;
- }
-
- $count;
- }
-
- 1;
-